let crypto = require('crypto');
let urllib = require('urllib');
const IGNORE_ERROR_CODES = {
	'isv.user-not-exist:invalid-nick': 1
};
module.exports = class extends think.Service {
	constructor(options) {
		super();
		const appkey = think.config('setup.alimamaAppid');
		const appsecret = think.config('setup.alimamaSecret');
		options = options || {
			appkey: appkey,
			appsecret: appsecret
		};
		this.REST_URL = options.REST_URL || "http://gw.api.taobao.com/router/rest";
		this.appkey = options.appkey;
		this.appsecret = options.appsecret;
	}

	/**
	 * hash
	 *
	 * @param {String} method hash method, e.g.: 'md5', 'sha1'
	 * @param {String|Buffer} s
	 * @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
	 * @return {String} md5 hash string
	 * @public
	 */
	hash(method, s, format) {
		let sum = crypto.createHash(method);
		let isBuffer = Buffer.isBuffer(s);
		if(!isBuffer && typeof s === 'object') {
			s = JSON.stringify(sortObject(s));
		}
		sum.update(s, isBuffer ? 'binary' : 'utf8');
		return sum.digest(format || 'hex');
	}

	/**
	 * md5 hash
	 *
	 * @param {String|Buffer} s
	 * @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
	 * @return {String} md5 hash string
	 * @public
	 */
	md5(s, format) {
		return this.hash('md5', s, format);
	}

	YYYYMMDDHHmmss(d, options) {
		d = d || new Date();
		if(!(d instanceof Date)) {
			d = new Date(d);
		}

		let dateSep = '-';
		let timeSep = ':';
		if(options) {
			if(options.dateSep) {
				dateSep = options.dateSep;
			}
			if(options.timeSep) {
				timeSep = options.timeSep;
			}
		}
		let date = d.getDate();
		if(date < 10) {
			date = '0' + date;
		}
		let month = d.getMonth() + 1;
		if(month < 10) {
			month = '0' + month;
		}
		let hours = d.getHours();
		if(hours < 10) {
			hours = '0' + hours;
		}
		let mintues = d.getMinutes();
		if(mintues < 10) {
			mintues = '0' + mintues;
		}
		let seconds = d.getSeconds();
		if(seconds < 10) {
			seconds = '0' + seconds;
		}
		return d.getFullYear() + dateSep + month + dateSep + date + ' ' +
			hours + timeSep + mintues + timeSep + seconds;
	}

	checkRequired(params, keys) {
		if(!Array.isArray(keys)) {
			keys = [keys];
		}
		for(let i = 0, l = keys.length; i < l; i++) {
			let k = keys[i];
			if(!params.hasOwnProperty(k)) {
				let err = new Error('`' + k + '` required');
				err.name = 'ParameterMissingError';
				return err;
			}
		}
	}

	getApiResponseName(apiName) {
		let reg = /\./g;
		if(apiName.match('^taobao')) {
			apiName = apiName.substr(7)
		}
		return apiName.replace(reg, '_') + '_response';
	}

	invoke(method, params, reponseNames, defaultResponse, type, callback) {
		params.method = method;
		this.request(params, type, function(err, result) {
			if(err) {
				return callback(err);
			}
			let response = result;
			if(reponseNames && reponseNames.length > 0) {
				for(let i = 0; i < reponseNames.length; i++) {
					let name = reponseNames[i];
					response = response[name];
					if(response === undefined) {
						break;
					}
				}
			}
			if(response === undefined) {
				response = defaultResponse;
			}
			callback(null, response);
		});
	}

	wrapJSON(s) {
		//s = s.replace(/(\d{16,})/g, "\"$&\"");
		let matchs = s.match(/id\"\:\s?\d{16,}/g);
		if(matchs) {
			for(let i = 0; i < matchs.length; i++) {
				let m = matchs[i];
				s = s.replace(m, 'id":"' + m.split(':')[1].trim() + '"');
			}
		}
		console.log(s);
		return s;
	}

	/**
	 * Request API.
	 *
	 * @param {Object} params
	 * @param {String} [type='GET']
	 * @param {Function(err, result)} callback
	 * @public
	 */
	request(params, type, callback) {
		if(typeof type === 'function') {
			callback = type;
			type = null;
		}
		let err = this.checkRequired(params, 'method');
		if(err) {
			return callback(err);
		}
		let args = {
			timestamp: this.timestamp(),
			format: 'json',
			app_key: this.appkey,
			v: '2.0',
			sign_method: 'md5'
		};
		for(let k in params) {
			if(typeof params[k] === 'object') {
				args[k] = JSON.stringify(params[k]);
			} else {
				args[k] = params[k];
			}
		}
		args.sign = this.sign(args);
		type = type || 'GET';
		let options = {
			type: type,
			data: args,
			agent: this.agent
		};
		let that = this;
		
		urllib.request(that.REST_URL, options, function(err, buffer) {
			let data;
			if(buffer) {
				
				buffer = that.wrapJSON(buffer.toString());
				try {
					data = JSON.parse(buffer);
				} catch(e) {
					err = e;
					e.data = buffer.toString();
					data = null;
				}
			}
			let errRes = data && data.error_response;
			if(errRes) {
				if(!errRes.sub_msg || !IGNORE_ERROR_CODES[errRes.sub_code]) {
					// no sub_msg error, let caller handle it.
					let msg = errRes.msg + ', code ' + errRes.code;
					if(errRes.sub_msg) {
						msg += '; ' + errRes.sub_code + ': ' + errRes.sub_msg;
					}
					err = new Error(msg);
					err.name = 'TOPClientError';
					err.code = errRes.code;
					err.sub_code = errRes.sub_code;
					err.data = buffer.toString();
					data = null;
				}
			}

			callback(err, data);
		});
	}

	/**
	 * Get now timestamp with 'yyyy-MM-dd HH:mm:ss' format.
	 * @return {String}
	 */
	timestamp() {
		return this.YYYYMMDDHHmmss();
	}

	/**
	 * Sign API request.
	 * see http://open.taobao.com/doc/detail.htm?id=111#s6
	 *
	 * @param  {Object} params
	 * @return {String} sign string
	 */
	sign(params) {
		let sorted = Object.keys(params).sort();
		let basestring = this.appsecret;
		for(let i = 0, l = sorted.length; i < l; i++) {
			let k = sorted[i];
			basestring += k + params[k];
		}
		basestring += this.appsecret;
		return this.md5(basestring).toUpperCase();
	}

	/**
	 * execute top api
	 */
	async execute(apiname, params) {
		const execute = () => {
			const deferred = think.defer();

			this.invoke(apiname, params, [this.getApiResponseName(apiname)], null, 'POST', function(error, response) {

				if(!error) {
					deferred.resolve(response);
				} else {
					deferred.resolve(error);
				}
			});
			return deferred.promise;
		};

		return await execute();

	}

}